package com.leonzhangxf.configuration;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zaxxer.hikari.HikariDataSource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.CuratorWatcher;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author leonzhangxf
 */
@Slf4j
@Configuration
public class DataSourceConfiguration implements InitializingBean {

    private final CuratorFramework client;

    private final ObjectMapper objectMapper;

    private static final String DATASOURCE_PATH = "/datasource";

    public DataSourceConfiguration(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
        this.client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .retryPolicy(new ExponentialBackoffRetry(1000, 3, 10000))
            .namespace("program_3").build();

        client.start();
    }

    public JdbcProperties getConfig() throws Exception {
        byte[] bytes = client.getData().forPath(DATASOURCE_PATH);
        return objectMapper.readValue(bytes, JdbcProperties.class);
    }

    private HikariDataSource dataSource;

    private final Object lock = new Object();

    @Bean
    public DataSource dataSource() throws Exception {
        JdbcProperties config = getConfig();
        log.info("datasource config: {}", config);
        dataSource = createDataSource(config);

        return (DataSource) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
            new Class[]{DataSource.class},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    synchronized (lock) {
                        return method.invoke(dataSource, args);
                    }
                }
            });
    }

    private static HikariDataSource createDataSource(JdbcProperties config) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(config.getUrl());
        dataSource.setDriverClassName(config.getDriver());
        dataSource.setUsername(config.getUsername());
        dataSource.setPassword(config.getPassword());
        return dataSource;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        client.getData().usingWatcher(new ConfigChangeCuratorWatcher(this)).forPath(DATASOURCE_PATH);
    }

    static class ConfigChangeCuratorWatcher implements CuratorWatcher {

        private final DataSourceConfiguration configuration;

        ConfigChangeCuratorWatcher(DataSourceConfiguration configuration) {
            this.configuration = configuration;
        }

        @Override
        public void process(WatchedEvent event) throws Exception {
            if (event.getState() == Watcher.Event.KeeperState.SyncConnected
                && event.getType() == Watcher.Event.EventType.NodeDataChanged) {
                JdbcProperties config = configuration.getConfig();
                log.info("datasource config change: {}", config);

                synchronized (configuration.lock) {
                    configuration.dataSource.close();
                    configuration.dataSource = createDataSource(config);
                    configuration.client.getData()
                        .usingWatcher(this).forPath(DATASOURCE_PATH);
                }
            }
        }
    }

    @Data
    public static class JdbcProperties {
        @JsonProperty("url")
        private String url;
        @JsonProperty("driver")
        private String driver;
        @JsonProperty("username")
        private String username;
        @JsonProperty("password")
        private String password;
    }
}
